// 参考 https://github.com/ustbhuangyi/storage/blob/master/src/index.js

// import { isObject } from 'core-js/fn/object'

/** 几种设想：
 * 1.添加 maxAge 和 ctime 键标识数据是否过期，再保存数据
 *   + 优点：简单直接
 *   + 缺点：容易缓存过多冗余(长期未使用且未清理)数据
 * 2.在 1 的基础上根据数据有效期设置定时器，定时清理过期数据
 *   + 优点：定时释放过期数据，保证本地存储空间
 *   + 缺点：定时器过多影响页面性能，且有的定时器会常驻很长时间(退出页面会阻断定时器？)
 *   + 适用：需要缓存、但生命周期短的数据
 * 3.在 1 的基础上，用 try...catch 捕获缓存已满时的错误，此时清理过期数据，再保存新数据
 *   + 优点：避免使用定时器的缺陷
 *   + 缺点：当没有过期数据时，与 1 无异
 * 4.在 3 的基础上，增加一个数据的权重(weight)键，若清理过期数据无法腾出空间时，尝试删除低权重的数据，再保存新数据
 *   + 优点：应该是当前比较好的方法
 *   + 适用：需要缓存、但生命周期较长的数据
 */

/** 属性和方法：(原生只有 key()/setItem()/getItem()/removeItem()/clear() 五个方法)
 * getItem(key)
 * setItem(key, val, options)
 *   + timeout：默认 false，是否定时清理过期缓存
 *   + weight：默认 0，缓存的权重
 * removeItem(key)
 * clear()
 * has(key)：是否有指定键名
 * hasCache(key)：是否未过期
 */

const isServer = typeof window === 'undefined'

const store = {
  storage: !isServer ? window.localStorage : null,
  session: {
    storage: !isServer ? window.sessionStorage : null
  }
}

const serialize = val => JSON.stringify(val)

const isExpire = (ctime, maxAge) => (new Date(ctime).getTime() + maxAge) <= Date.now()

const isEmpty = v => (v === null || v === undefined)

const deserialize = val => {
  if (typeof val !== 'string') {
    return undefined
  }
  try {
    return JSON.parse(val)
  } catch (e) {
    return val || null
  }
}

/** 维护一个权重对象列表
 * {
 *   '0': ['name', 'age'],
 *   '1': ['x-token']
 * }
 */
const weightObj = {}

const api = {
  has (key) {
    return !!this.getItem(key)
  },

  keys () {
    const keys = []
    let i = 0
    while (this.storage.key(i)) {
      keys.push(this.storage.key(i++))
    }
    return keys
  },

  /**
   * 判断是否有缓存
   * @param {*} key
   */
  hasCache (key) {
    if (this.has(key)) {
      const data = this.getItem(key)
      if (isEmpty(data.ctime) || isEmpty(data.maxAge)) {
        return false
      }
      return !isExpire(data.ctime, data.maxAge)
    }

    return false
  },

  /**
   * 判断数据是否被要求缓存，根据缓存标识(ctime, maxAge)
   * @param {*} key
   */
  isCache (key) {
    if (this.has(key)) {
      const data = this.getItem(key)
      return !(isEmpty(data.ctime) || isEmpty(data.maxAge))
    }

    return false
  },

  setItem (key, val, cache = false, {
    ctime = Date.now(),
    maxAge = 60 * 1000,
    weight = 0,
    timeout = false
  } = {}) {
    console.log('[storage] try to set item:', key)
    // 不需要缓存时
    if (!cache) {
      return this.storage.setItem(key, serialize(val))
    }
    /* if (!val) {
      return this.removeItem(key)
    } */
    // 真实数据：format.data
    const format = Object.assign({}, { data: val, ctime, maxAge, weight })
    try {
      this.storage.setItem(key, serialize(format))
    } catch (e) {
      console.error(`[storage] Try to delete items that was expired. Error: ${e}`)
      this.removeExpired()
      try {
        this.storage.setItem(key, serialize(format))
      } catch (e) {
        console.error(`[storage] Try to delete items by weight. Error: ${e}`)
        this.removeLowerWeight()
        try {
          this.storage.setItem(key, serialize(format))
        } catch (e) {
          console.error(e)
          throw new Error('[storage] Can\'t set storage, unknow error.')
        }
      }
    } finally {
      // 存至权重对象列表
      if (weightObj[weight]) {
        weightObj[weight].push(key)
      } else {
        weightObj[weight] = [key]
      }
    }

    // 超时自动删除
    // eslint-disable-next-line no-extra-boolean-cast
    if (!!timeout) {
      setTimeout(() => {
        this.removeItem(key)
      }, maxAge)
    }
    return format
  },

  getItem (key) {
    /** fix: Maximum call stack size exceeded
     * when use this.has(key)
     */
    const data = deserialize(this.storage.getItem(key))
    return data === undefined ? null : data
  },

  removeItem (key) {
    this.storage.removeItem(key)
  },

  clear () {
    this.storage.clear()
  },

  getWeight (key) {
    if (this.has(key) && this.isCache(key)) {
      return deserialize(this.storage.getItem(key)).weight
    }

    return null
  },

  /**
   * 删除过期的数据
   */
  removeExpired () {
    this.keys().forEach(v => {
      // 数据必须有缓存标识，且超出缓存时间
      if (this.isCache(v) && !this.hasCache(v)) {
        this.removeItem(v)
      }
    })
  },

  /** 删除低权重的数据
   * 方法：删除最低权重的所有数据(最坏情况下会删除所有缓存)
   * 待优化：只删除部分低权重的数据，只要有足够空间让新数据保存即可
   */
  removeLowerWeight () {
    const lowWeight = Object.keys(weightObj).sort((a, b) => (+a) - (+b))[0]
    const delKey = weightObj[lowWeight]
    for (let i = 0, l = delKey.length; i < l; i++) {
      this.removeItem(delKey(i))
    }
  }
}

Object.assign(store, api)
Object.assign(store.session, api)

// 检测 storage 是否被禁用
/* try {
  const testKey = '__store__'
  store.set(testKey, testKey)
  if (store.get(testKey) !== testKey) {
    store.disabled = true
  }
  store.remove(testKey)
} catch (e) {
  store.disabled = true
} */

export default store
